﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using System.Web.Helpers;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;

namespace MvcExtensions.Extensions
{
    public static class KnockoutControlExtensions
    {
        public static MvcHtmlString KOPartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    string partialViewName)
        {
            var name = ExpressionHelper.GetExpressionText(expression);
            var model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
            var viewData = new ViewDataDictionary(helper.ViewData)
            {
                TemplateInfo = new TemplateInfo
                {
                    HtmlFieldPrefix = name
                }
            };

            var oldModelName = RenderingViewModel;

            RenderingViewModel = string.IsNullOrEmpty(oldModelName)
                                     ? name
                                     : string.Format("{0}.{1}", oldModelName, name);

            var mvcHtmlString = helper.Partial(partialViewName, model, viewData);

            RenderingViewModel = oldModelName;

            return mvcHtmlString;
        }

        public static MvcHtmlString KOWithBindingFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
                                                                        Expression<Func<TModel, TProperty>> expression,
                                                                        bool bindingWithModelName = true)
        {
            var bindingPath = string.Format("data-bind=\"with: {0}\"",
                                            GetDataBindingPath(helper, expression, bindingWithModelName));
            return MvcHtmlString.Create(bindingPath);
        }

        public static MvcHtmlString KOForeEachBindingFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
                                                                            Expression<Func<TModel, TProperty>>
                                                                                expression,
                                                                            bool bindingWithModelName = true)
        {
            var bindingPath = string.Format("data-bind=\"foreach: {0}\"",
                                            GetDataBindingPath(helper, expression, bindingWithModelName));
            return MvcHtmlString.Create(bindingPath);
        }

        public static MvcHtmlString KODropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                         Expression<Func<TModel, TProperty>> expression,
                                                                         object htmlAttributes = null)
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind",
                              string.Format(
                                  "value:{0}.{1}, options: {1}List, optionsText: 'Text', optionsValue: 'Value'",
                                  modelName, propertyName));

            var selectListItems = ControlExtensions.GetSelectListItems(htmlHelper, expression).ToList();

            var knockoutViewModelDictionary = GetKnockoutViewModelDictionary(htmlHelper);

            if (!knockoutViewModelDictionary.ContainsKey(propertyName + "List"))
            {
                var sbJson = GetListViewModelJson(propertyName, selectListItems);
                knockoutViewModelDictionary.Add(propertyName + "List", sbJson);
            }

            return htmlHelper.DropDownListFor(expression, selectListItems, attributeList);
        }

        public static MvcHtmlString KODropDownListFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                          Expression<Func<TModel, TProperty>> expression,
                                                                          IEnumerable<SelectListItem> selectListItems = null,
                                                                          object htmlAttributes = null,
                                                                          string binding = "value",
                                                                          bool bindingWithModelName = true,
                                                                          KOBindingOptions options = null)
        {
            selectListItems = selectListItems ?? ControlExtensions.GetSelectListItems(htmlHelper, expression).ToList();

            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList, options: options);

            return htmlHelper.DropDownListFor(expression, selectListItems, attributeList);
        }

        public static MvcHtmlString KOTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    object htmlAttributes = null,
                                                                    string binding = "value",
                                                                    bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            return htmlHelper.TextBoxFor(expression, attributeList);
        }

        public static MvcHtmlString KOPasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    object htmlAttributes = null,
                                                                    string binding = "value",
                                                                    bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            return htmlHelper.PasswordFor(expression, attributeList);
        }

        public static MvcHtmlString KOTextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                     Expression<Func<TModel, TProperty>> expression,
                                                                     int rows = 4, int columns = 50,
                                                                     object htmlAttributes = null,
                                                                     string binding = "value",
                                                                     bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            return htmlHelper.TextAreaFor(expression, attributeList);
        }

        public static MvcHtmlString KODateTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                        Expression<Func<TModel, TProperty>> expression,
                                                                        object htmlAttributes = null,
                                                                        string binding = "value",
                                                                        bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            if (!attributeList.ContainsKey("class"))
            {
                attributeList.Add("class", "date");
            }

            return htmlHelper.TextBoxFor(expression, "{0:dd/MM/yyyy}", attributeList);
        }

        public static MvcHtmlString KOCheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper,
                                                          Expression<Func<TModel, bool>> expression,
                                                          object checkboxAttributes = null,
                                                          object labelAttributes = null,
                                                          string binding = "checked",
                                                          bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(checkboxAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            var htmlString = htmlHelper.CheckBoxFor(expression, attributeList);

            var displayName = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).DisplayName;

            var lblAttributes = ConvertDictionaryToAttributes(GetAttributeList(labelAttributes));

            return MvcHtmlString.Create(string.Format("<{0} {3}>{1}{2}</{0}>", "label",
                                                      htmlString, displayName,
                                                      lblAttributes));
        }

        public static MvcHtmlString KORadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                        Expression<Func<TModel, TProperty>> expression,
                                                                        object htmlAttributes = null,
                                                                        object groupHtmlAttributes = null,
                                                                        string binding = "checked",
                                                                        bool bindingWithModelName = true,
                                                                        bool uniqueName = false)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList, uniqueName);

            var selectListItems = ControlExtensions.GetSelectListItems(htmlHelper, expression).ToList();

            var groupHtmlAttributeList = GetAttributeList(groupHtmlAttributes);

            var groupHtmlAttributeString = ConvertDictionaryToAttributes(groupHtmlAttributeList);

            var htmlStringbuilder = new StringBuilder(1024);
            htmlStringbuilder.AppendFormat("<div{0}>", groupHtmlAttributeString);

            //select first if none of are selected.
            if (!selectListItems.Any(x => x.Selected) && selectListItems.Any())
            {
                selectListItems.First().Selected = true;
            }

            foreach (var item in selectListItems)
            {
                if (item.Selected)
                {
                    attributeList.Add("checked", "checked");
                }
                var radioButtonString = InputExtensions.RadioButtonFor(htmlHelper,
                                                                       expression,
                                                                       item.Value,
                                                                       attributeList);
                htmlStringbuilder.AppendFormat("<div><label>{0}{1}</label></div>", radioButtonString, item.Text);
            }

            htmlStringbuilder.Append("</div>");

            return MvcHtmlString.Create(htmlStringbuilder.ToString());
        }

        public static MvcHtmlString KOHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                   Expression<Func<TModel, TProperty>> expression,
                                                                   string binding = "value",
                                                                   bool bindingWithModelName = true)
        {
            var attributeList = new RouteValueDictionary();
            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            return InputExtensions.HiddenFor(htmlHelper, expression, attributeList);
        }

        public static MvcHtmlString KODisplayFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    object htmlAttributes = null,
                                                                    string binding = "text",
                                                                    bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            var value = htmlHelper.DisplayFor(expression, attributeList);

            var attributes = ConvertDictionaryToAttributes(attributeList);

            return MvcHtmlString.Create(string.Format("<span {1}>{0}</span>", value, attributes));
        }

        public static MvcHtmlString KODisplayLabelValueFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                              Expression<Func<TModel, TProperty>> expression,
                                                                              object htmlAttributes = null,
                                                                              string binding = "text",
                                                                              bool bindingWithModelName = true)
        {
            var attributeList = GetAttributeList(htmlAttributes);

            AddDataBinding(htmlHelper, expression, binding, bindingWithModelName, attributeList);

            var display = htmlHelper.DisplayNameFor(expression);
            var value = htmlHelper.DisplayFor(expression, attributeList);

            var attributes = ConvertDictionaryToAttributes(attributeList);

            return MvcHtmlString.Create(string.Format("{0} : <span {2}>{1}</span>", display, value, attributes));
        }

        public static void SetKOBindingWithModelName<TModel>(this HtmlHelper<TModel> htmlHelper, bool withModelName)
        {
            HttpContext.Current.Items[KOBindingWithModelNameString] = withModelName;
        }

        private const string KOBindingWithModelNameString = "KOBindingWithModelName";

        public static bool KOBindingWithModelName
        {
            get
            {
                var httpContext = HttpContext.Current;
                if (!httpContext.Items.Contains(KOBindingWithModelNameString))
                {
                    //default is on
                    return true;
                }
                return (bool)httpContext.Items[KOBindingWithModelNameString];
            }
        }

        public static MvcHtmlString SetKOBindingOptions(this HtmlHelper htmlHelper, KOBindingOptions options)
        {
            return options != null
                       ? MvcHtmlString.Create(options.ToBindingString())
                       : MvcHtmlString.Empty;
        }

        public static string ConvertDictionaryToAttributes(IDictionary<string, object> dictionary)
        {
            var attributes = dictionary.Any() ? " " : string.Empty;
            return dictionary.Aggregate(attributes, (c, att) => c + string.Format("{0}=\"{1}\"", att.Key, att.Value));
        }


        private static void AddDataBinding<TModel, TProperty>(HtmlHelper<TModel> htmlHelper,
                                                              Expression<Func<TModel, TProperty>> expression,
                                                              string binding,
                                                              bool bindingWithModelName,
                                                              IDictionary<string, object> attributeList,
                                                              bool uniqueName = false,
                                                              KOBindingOptions options = null)
        {
            var propertyName = GetDataBindingPath(htmlHelper, expression, bindingWithModelName);

            if (options != null)
            {
                attributeList.Add(KOBindingOptions.KODataBind, options.ToParams());
            }

            if (attributeList.ContainsKey(KOBindingOptions.KODataBind))
            {
                var existingKoBinding = attributeList[KOBindingOptions.KODataBind];
                propertyName = string.Format("{0},{1}", propertyName, existingKoBinding);
                attributeList.Remove(KOBindingOptions.KODataBind);
            }

            attributeList.Add(KOBindingOptions.KODataBind,
                              uniqueName
                                  ? string.Format("{0}: {1}, uniqueName: true", binding, propertyName)
                                  : string.Format("{0}: {1}", binding, propertyName));
        }

        private static string GetDataBindingPath<TModel, TProperty>(HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    bool bindingWithModelName)
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            if (bindingWithModelName && KOBindingWithModelName)
            {
                var modelName = htmlHelper.GetKOModelName();
                return string.Format("{0}.{1}", modelName, propertyName);
            }
            return propertyName;
        }

        private static RouteValueDictionary GetAttributeList(object htmlAttributes)
        {
            return htmlAttributes != null
                       ? HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)
                       : new RouteValueDictionary();
        }

        public const string KnockoutViewModelString = "KnockOutViewModel";

        public static void KOBuildViewModel<TModel>(this HtmlHelper<TModel> htmlHelper)
        {
            KOViewModelBuilder.BuildViewModel(htmlHelper.ViewData.Model.GetType());
            //var modelName = htmlHelper.GetKOModelName();
        }

        public static StringBuilder GetListViewModelJson(string propertyName, IEnumerable<SelectListItem> selectListItems)
        {
            var koViewModel = new StringBuilder(string.Format("\n\t\t{0}.{1}List = [", KnockoutViewModelString, propertyName));

            foreach (var listItem in selectListItems)
            {
                koViewModel.Append('{');
                koViewModel.AppendFormat("Value:'{0}', Text:'{1}',", listItem.Value, listItem.Text);
                koViewModel.Append("},");
            }
            koViewModel.Append("];");
            return koViewModel;
        }

        public static MvcHtmlString KOGenerateViewModel<TModel>(this HtmlHelper<TModel> htmlHelper,
                                                                bool applyBindings = false)
        {
            if (!htmlHelper.ViewContext.HttpContext.Items.Contains(KnockoutViewModelString))
                return MvcHtmlString.Empty;

            var modelName = htmlHelper.GetKOModelName();

            var knockoutViewModelDictionary = GetKnockoutViewModelDictionary(htmlHelper);

            if (knockoutViewModelDictionary == null)
                return MvcHtmlString.Empty;

            var koViewModelScript =
                new StringBuilder(string.Format("\n\tif ({0} === undefined) {{\n\t var {0} = {{}};\n\t}}",
                                                KnockoutViewModelString));

            foreach (var kp in knockoutViewModelDictionary)
            {
                koViewModelScript.Append(kp.Value);
            }

            koViewModelScript.AppendFormat("\n\t{0}.{1} = new {2}();\n",
                                           KnockoutViewModelString,
                                           modelName,
                                           htmlHelper.ViewData.Model.GetType().Name);
            if (applyBindings)
            {
                koViewModelScript.Append(
                    string.Format("$(document).ready(function () {{\n\tko.applyBindings({0});\n\t}});",
                                  KnockoutViewModelString));
            }
            return MvcHtmlString.Create(koViewModelScript.ToString());
        }

        public static Dictionary<string, StringBuilder> KnockoutViewModelDictionary
        {
            get
            {
                var httpContext = HttpContext.Current;
                if (!httpContext.Items.Contains(KnockoutViewModelString))
                {
                    var objectDictionary = new Dictionary<string, StringBuilder>();
                    httpContext.Items.Add(KnockoutViewModelString, objectDictionary);
                    return objectDictionary;
                }
                return httpContext.Items[KnockoutViewModelString] as Dictionary<string, StringBuilder>;

            }
        }

        #region private methods and properties

        private static Dictionary<string, StringBuilder> GetKnockoutViewModelDictionary<TModel>(HtmlHelper<TModel> htmlHelper)
        {
            if (!htmlHelper.ViewContext.HttpContext.Items.Contains(KnockoutViewModelString))
            {
                var objectDictionary = new Dictionary<string, StringBuilder>();
                htmlHelper.ViewContext.HttpContext.Items.Add(KnockoutViewModelString, objectDictionary);
                return objectDictionary;
            }
            return htmlHelper.ViewContext.HttpContext.Items[KnockoutViewModelString] as Dictionary<string, StringBuilder>;
        }

        private const string RenderingViewModelString = "RenderingViewModel";

        private static string RenderingViewModel
        {
            get
            {
                var httpContext = HttpContext.Current;
                if (httpContext.Items.Contains(RenderingViewModelString))
                {
                    return httpContext.Items[RenderingViewModelString] as string;
                }
                return string.Empty;
            }
            set
            {
                var httpContext = HttpContext.Current;
                if (!httpContext.Items.Contains(RenderingViewModelString))
                {
                    httpContext.Items.Add(RenderingViewModelString, value);
                }
                else
                {
                    httpContext.Items[RenderingViewModelString] = value;
                }
            }
        }

        private static string GetKOModelName<TModel>(this HtmlHelper<TModel> helper)
        {
            if (string.IsNullOrEmpty(RenderingViewModel))
            {
                RenderingViewModel = helper.ViewData.Model.GetType().Name.Replace("ViewModel", "");
            }

            return RenderingViewModel;

            //var prefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix;
            //return string.IsNullOrEmpty(prefix) ? RenderingViewModel : string.Format("{0}.{1}", RenderingViewModel, prefix);
        }

        #endregion


        public static MvcHtmlString GenerateJsonModel<TModel>(this HtmlHelper<TModel> htmlHelper, string model = "model")
        {
            const string jsonModelFormat = "var {0} = {1};";

            var jsonData = htmlHelper.Raw(Json.Encode(htmlHelper.ViewData.Model));

            return MvcHtmlString.Create(string.Format(jsonModelFormat, model, jsonData));

        }
    }


    public class KOBindingOptions
    {
        public const string KODataBind = "data-bind";

        public string Value { get; set; }
        public string Checked { get; set; }
        public string DateString { get; set; }
        public string Text { get; set; }
        public string Click { get; set; }
        public string Visible { get; set; }
        public string Options { get; set; }
        public string OptionsText { get; set; }
        public string OptionsValue { get; set; }

        private const string KOBindingString = "data-bind=\"{0}\"";

        public string ToBindingString()
        {
            return string.Format(KOBindingString, ToParams());
        }

        public string ToParams()
        {
            var sb = new StringBuilder(128);
            if (!string.IsNullOrEmpty(Value)) sb.AppendFormat("value:{0},", Value);
            if (!string.IsNullOrEmpty(Checked)) sb.AppendFormat("checked:{0}, ", Checked);
            if (!string.IsNullOrEmpty(DateString)) sb.AppendFormat("dateString:{0}, ", DateString);
            if (!string.IsNullOrEmpty(Text)) sb.AppendFormat("text:{0}, ", Text);
            if (!string.IsNullOrEmpty(Click)) sb.AppendFormat("click:{0}, ", Click);
            if (!string.IsNullOrEmpty(Visible)) sb.AppendFormat("visible:{0}, ", Visible);
            if (!string.IsNullOrEmpty(Options)) sb.AppendFormat("options:{0}, ", Options);
            if (!string.IsNullOrEmpty(OptionsText)) sb.AppendFormat("optionsText:{0}, ", OptionsText);
            if (!string.IsNullOrEmpty(OptionsValue)) sb.AppendFormat("optionsValue:{0}, ", OptionsValue);

            //remove last ', '
            if (sb.Length > 2)
            {
                sb.Remove(sb.Length - 2, 2);
            }
            return sb.ToString();
        }

        public override string ToString()
        {
            return ToBindingString();
        }
    }

}